海南老脚数

vuePress-theme-reco 海南老脚数    2017 - 2021
海南老脚数 海南老脚数

Choose mode

  • dark
  • auto
  • light
主页
指南
  • 应用介绍
  • cH5-PWA应用 (opens new window)
  • SSR-个人官网 (opens new window)
  • 微前端框架应用 (opens new window)
印记
高级
  • 小程序Node后端实践
  • JS开发灵活的数据应用
  • Node核心知识
  • Git原理详解及实战
进阶
  • 大厂H5开发实战
  • 前端性能优化
  • 前端面试指南
组件库
  • Vue3.0
  • Nuxt
  • 吃吃吃
分类
  • 问题集中营
  • VUE
  • 前端小笔记
  • Cookie
  • 深夜食堂
标签
Github (opens new window)
掘金 (opens new window)
author-avatar

海南老脚数

5

文章

4

标签

主页
指南
  • 应用介绍
  • cH5-PWA应用 (opens new window)
  • SSR-个人官网 (opens new window)
  • 微前端框架应用 (opens new window)
印记
高级
  • 小程序Node后端实践
  • JS开发灵活的数据应用
  • Node核心知识
  • Git原理详解及实战
进阶
  • 大厂H5开发实战
  • 前端性能优化
  • 前端面试指南
组件库
  • Vue3.0
  • Nuxt
  • 吃吃吃
分类
  • 问题集中营
  • VUE
  • 前端小笔记
  • Cookie
  • 深夜食堂
标签
Github (opens new window)
掘金 (opens new window)
  • 开篇介绍:Node 10 年大跃进与当下在互联网研发中的地位
  • 源码挖掘: Webpack 中用到 Node 的 10 个核心基础能力
  • [命令行动画龟兔赛跑] Node 的语言基础 - JS(ES5/6/7/8)
  • [视频时长统计] Node 的模块机制(CommonJS)与包管理
  • [发布 LTS 查看工具] Node 的生态利器 - NPM
  • [中英文 JSON 合并工具] Node 的文件操作能力 - fs
  • [实现一个音乐播放器] Node 的事件机制 - EventEmitter
  • [图片拷贝小工具] Node 的编码与缓冲 - Buffer
  • [视频流转 MP3 工具] Node 数据流与管道 - Stream/pipe
  • [静态资源服务器] Node 的工具集 - path/util/zlib 等
  • [实现 N 个 API/网页爬虫] Node 的 HTTP 处理 - 请求与响应
  • [压测 Cluster 的并发负载] Node 的集群 - cluster
  • [埋点搜集服务器] - 总结: Koa 服务端框架用到了哪些能力
  • 源码解读:Node 的程序架构及启动流程

vuePress-theme-reco 海南老脚数    2017 - 2021

[埋点搜集服务器] - 总结: Koa 服务端框架用到了哪些能力

海南老脚数

# [埋点搜集服务器] - 总结: Koa 服务端框架用到了哪些能力

本节目标: 「实现一个简单的埋点服务器」 站在巨人的肩膀上,不仅登高望远,可以低头俯瞰,Node 有很多优秀框架,赋予我们极大的工程效率,那么前面章节学习到的 Node 基础能力在 Koa 这个框架里到底用到了哪些呢?我们拨开云雾看一看。
1

对于 Node 的框架部分,我们本册只针对 Koa 简单学习一下,因为它的源码更精简,结构更清晰,学习的难度相对较小,Koa 的历史就不多说了,也是 TJ 开创,从 GeneratorFunction 时代到现在的 Async/Await 异步时代,经历了一个较大的版本变化,大家可以翻开 Koa 5 年前的代码 (opens new window) 看一看,早期的 Koa 就分离了 Application 和 Context,代码风格和流程的设计就比较精简,对于进化到今天的 Koa ,在它里面像 cookies koa-compose delegates 都尽量抽象出去了,所以剩下的部分,特别的纯粹,只有:

  • application 整个 Koa 应用服务类
  • context Koa 的应用服务上下文
  • request Koa 的请求对象
  • response Koa 的响应对象

这四个也分别对应到 Koa 的四个模块文件: application.js (opens new window)、context.js (opens new window)、request.js (opens new window)、response.js (opens new window),整个 HTTP 的门面就是靠它们扛起来的,代码量加一起也就一两千行,简约而强大,整个 Koa 的设计哲学就是小而美的,比如 Koa 启动一个 HTTP Server 也非常简单:

const Koa = require('koa')
const app = new Koa()

app.use(ctx => {
  ctx.body = 'Hello Koa'
}).listen(3000)
1
2
3
4
5
6

在 new 这个 Koa 实例的时候,实际上是基于这个 Application 类创建的实例,而这个类集成了 Emitter 也就是我们前文学习到的事件类,一旦继承了事件,就可以非常方便基于各种条件来监听和触发事件了。

module.exports = class Application extends Emitter {
  constructor() {
    super()
    this.proxy = false
    this.middleware = []
    this.subdomainOffset = 2
    this.env = process.env.NODE_ENV || 'development'
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
1
2
3
4
5
6
7
8
9
10

在创建这个实例的时候,对实例上面也挂载了通过 Object.create 所创建出来的上下文对象 context、请求对象 request 和 响应对象 response,所以一开始,这几个核心的对象都有了,后面无非就是基于这些对象做更多的扩展和封装罢了。

为帮助大家读源码,我把 application.js 删减到了 100 行代码,单独拎出来给大家看一下:

const onFinished = require('on-finished')
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
const Emitter = require('events')
const util = require('util')
const Stream = require('stream')
const http = require('http')
const only = require('only')

// 继承 Emitter,暴露一个 Application 类
module.exports = class Application extends Emitter {
  constructor() {
    super()
    this.proxy = false
    this.middleware = []
    this.subdomainOffset = 2
    this.env = process.env.NODE_ENV || 'development'
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect
    }
  }

  // 等同于 http.createServer(app.callback()).listen(...)
  listen(...args) {
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }

  // 返回 JSON 格式数据
  toJSON() {
    return only(this, ['subdomainOffset', 'proxy', 'env'])
  }

  // 把当前实例 JSON 格式化返回
  inspect() { return this.toJSON() }

  // 把中间件压入数组
  use(fn) {
    this.middleware.push(fn)
    return this
  }

  // 返回 Node 原生的 Server request 回调
  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  // 在回调中处理 request 请求对象
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    // 先设置一个 404 的响应码,等后面来覆盖它
    res.statusCode = 404
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }

  // 初始化一个上下文,为 req/res 建立各种引用关系,方便使用
  createContext(req, res) {
    const context = Object.create(this.context)
    const request = context.request = Object.create(this.request)
    const response = context.response = Object.create(this.response)
    context.app = request.app = response.app = this
    context.req = request.req = response.req = req
    context.res = request.res = response.res = res
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
  }
}

// 响应处理的辅助函数
function respond(ctx) {
  const res = ctx.res
  let body = ctx.body
  // 此处删减了代码,如 head/空 body 等问题的处理策略等
  // 基于 Buffer/string 和 流,分别给予响应
  if (Buffer.isBuffer(body)) return res.end(body)
  if ('string' == typeof body) return res.end(body)
  if (body instanceof Stream) return body.pipe(res)

  // 最后则是以 JSON 的格式返回
  body = JSON.stringify(body)
  res.end(body)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

这个里面,我们关注到这几个点就可以了:

  • new Koa() 的 app 只有在 listen 的时候才创建 HTTP Server
  • use fn 的时候,传入的一个函数会被压入到中间件队列,像洋葱的一层层皮一样逐级进入逐级穿出
  • Koa 支持对 Buffer/String/JSON/Stream 数据类型的响应
  • 上下文 context 是在 Node 原生的 request 进入也就是异步回调执行的时候才创建,不是一开始创建好的,所以每个请求都有独立的上下文,自然不会互相污染
  • 创建好的上下文,Koa 会把它们跟原生,以及请求和响应之间,建立各种引用关系,方便在业务代码和中间件中使用,也就是 createContext 里面所干的事情

看到这里,Koa 这个服务框架对我们就没那么神秘了,索性再把 context.js 代码删减到 50 行,大家再浏览下:

const util = require('util')
const delegate = require('delegates')
const Cookies = require('cookies')

// 上下文 prototype 的原型
const proto = module.exports = {
  // 挑选上下文的内容,JSON 格式化处理后返回
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    }
  },

  // 错误捕获处理
  onerror(err) {},
  // 拿到 cookies
  get cookies() {},
  // 设置 cookies
  set cookies(_cookies) { }
}

// 对新版 Node 增加自定义 inspect 的支持
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect
}

// 为响应对象绑定原型方法
delegate(proto, 'response')
  .method('attachment').method('redirect').method('remove').method('vary')
  .method('set').method('append').method('flushHeaders')
  .access('status').access('message').access('body').access('length').access('type')
  .access('lastModified').access('etag')
  .getter('headerSent').getter('writable')


// 为请求对象绑定原型方法
delegate(proto, 'request')
  .method('acceptsLanguages').method('acceptsEncodings').method('acceptsCharsets')
  .method('accepts').method('get').method('is')
  .access('querystring').access('idempotent').access('socket').access('search')
  .access('method').access('query').access('path').access('url').access('accept')
  .getter('origin').getter('href').getter('subdomains').getter('protocol').getter('host')
  .getter('hostname').getter('URL').getter('header').getter('headers').getter('secure')
  .getter('stale').getter('fresh').getter('ips').getter('ip')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

可以发现,之所以 Koa 的请求/响应上下文上有那么多方法和属性可以用,或者可以设置,其实就是这里的 delegate 搞的鬼,它对 context 施加了许多能力,剩下的 request.js 和 response.js 就留给大家自行消化了,都是一些属性方法的特定封装,没有太多的门槛,或者大家可以参考这张图:

其中本册子所讲的几个知识点,在 Koa 中也有大量的使用,比如 path/util/stream/fs/http 等等,不过建议大家学习 Koa 时候重点关注它的网络进出模型,不需要过多关注底层细节。

# 编程练习 - 开发一个埋点服务器

我们可以用 Koa 开发一个简易的埋点收集服务器,客户端每请求一次,就把埋点的数据往数据库里更新一下,为了演示,我们使用 JSON 结构存储的 lowdb 来模拟数据库,大家可以在本地自行替换为 MongoDB 或者 MySQL,同时我们会用到 Koa 的一个路由中间件,首先我们把依赖的模块安装一下:

npm i koa koa-router lowdb -S
1

然后创建一个 server.js,代码如下:

const Koa = require('koa')
const path = require('path')
const Router = require('koa-router')
const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')

// 创建一个 Koa 服务实例
const app = new Koa()
// 创建一个路由的实例
const router = new Router()
// 创建一个数据库实例,这里用 lowdb 的 JSON 存储来模拟数据库而已
const adapter = new FileSync(path.resolve(__dirname, './db.json'))
const db = low(adapter)

// 初始化数据库,可以看做是数据库的字段定义
db.defaults({visits: [], count: 0}).write()

// 当有请求进来,路由中间件的异步回调会被执行
router.get('/', async (ctx, next) => {
  const ip = ctx.header['x-real-ip'] || ''
  const { user, page, action } = ctx.query

  // 更新数据库
  db.get('visits').push({ip, user, page, action}).write()
  db.update('count', n => n + 1).write()

  // 返回更新后的数据库字段
  ctx.body = {success: 1, visits: db.get('count')}
})

// 把中间件压入队列,等待执行
app
  .use(router.routes())
  .use(router.allowedMethods())
  .listen(7000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

在命令行 node server.js 把服务开起来后,可以从浏览器通过: [http://localhost:7000/?user=a&page=1&action=click](http://localhost:7000/?user=a&page=1&action=click) 来访问,或者从命令里面 curl [http://localhost:7000/?user=a&page=1&action=click](http://localhost:7000/?user=a&page=1&action=click),多请求几次,就会发现数据都存进去了,在 server.js 的同目录,有一个 db.json,里面的数据大概如下:

{
  "visits": [
    {
      "ip": "",
      "user": "a",
      "page": "1",
      "action": "click"
    },
    {
      "ip": "",
      "user": "a",
      "page": "1",
      "action": "click"
    }
  ],
  "count": 2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

那么本节给大家布置一个小作业,如果把上一节的 cluster 跟这一节的埋点服务器做结合,来提升服务器的负载能力,代码可以怎么写呢?

欢迎来到 海南老脚数
看板娘